package com.crc64.comm.serialcomm;

import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class SerialComm implements SerialPortEventListener, AutoCloseable {

	private final static Logger LOGGER = Logger.getLogger(SerialComm.class.getName());

	private static final int BUFFER_FILLOUT_DELAY_MS = 20;
	private static final int TIME_OUT = 5000;

	private final List<SerialCommListener> serialCommListeners = new ArrayList<SerialComm.SerialCommListener>();

	private SerialPort serialPort;
	private boolean isOpen;
	private DataInputStream dataInputStream;
	private DataOutputStream dataOutputStream;

	private final String portName;
	private final DataRate dataRate;
	private final DataBit dataBit;
	private final StopBit stopBit;
	private final Parity parity;
	private final byte[] rxMessage;

	public SerialComm(String portName, DataRate dataRate, DataBit dataBit, StopBit stopBit, Parity parity, byte[] rxMessage) {
		this.portName = portName;
		this.dataRate = dataRate;
		this.dataBit = dataBit;
		this.stopBit = stopBit;
		this.parity = parity;
		this.rxMessage = rxMessage;
	}

	public void open() throws Exception {
		if (!isOpen) {
			CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
			serialPort = (SerialPort) portIdentifier.open(SerialComm.class.getName(), TIME_OUT);
			serialPort.setSerialPortParams(dataRate.getDataRate(), dataBit.getDataBit(), stopBit.getStopBit(), parity.getParity());
			serialPort.addEventListener(this);
			serialPort.notifyOnDataAvailable(true);
			dataInputStream = new DataInputStream(serialPort.getInputStream());
			dataOutputStream = new DataOutputStream(serialPort.getOutputStream());
			isOpen = true;
			LOGGER.log(Level.INFO, "Serial port opened");
		}
	}

	public void sendMessage(byte[] message) throws IOException {
		if (isOpen) {
			dataOutputStream.write(message);
			dataOutputStream.flush();
		}
	}

	@Override
	public void serialEvent(SerialPortEvent event) {
		if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
			try {
				Thread.sleep(BUFFER_FILLOUT_DELAY_MS);
				dataInputStream.read(rxMessage);
			} catch (InterruptedException | IOException e) {
				e.printStackTrace();
			}
			notifySerialCommListeners(rxMessage);
		}
	}

	@Override
	public void close() throws Exception {
		if (isOpen) {
			serialPort.removeEventListener();
			serialPort.close();
			isOpen = false;
			LOGGER.log(Level.INFO, "Serial port closed");
		}
	}

	public boolean isOpen() {
		return isOpen;
	}

	public boolean addSerialCommListener(SerialCommListener listener) {
		return serialCommListeners.add(listener);
	}

	public boolean removeSerialCommListener(SerialCommListener listener) {
		return serialCommListeners.remove(listener);
	}

	private void notifySerialCommListeners(byte[] message) {
		for (SerialCommListener serialCommListener : serialCommListeners) {
			serialCommListener.newMessageReceived(message);
		}
	}

	public static interface SerialCommListener {

		public abstract void newMessageReceived(byte[] message);

	}

}
